/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.internal.config;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jtell.config.Configuration;
import org.jtell.config.EventSinkMetadata;
import org.jtell.internal.ReflectionUtils;
import org.jtell.internal.Guard;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * <p>
 * <code>DefaultConfiguration</code> is the default implementation of the {@link Configuration} interface. It is
 * suitable for use with most configuration types. It maintains a mutable internal map of event sink metadata which is
 * safe for concurrent access.
 * </p>
 * <p>
 * <strong>Thread Safety</strong><br/>
 * Instances of this class are safe for multithreaded access. 
 * </p>
 */
public class DefaultConfiguration implements Configuration
{
    private static Log LOG = LogFactory.getLog(DefaultConfiguration.class);

    /**
     * <p>
     * Internal map of listener classes to {@link Set}s of {@link EventSinkMetadata} instances. 
     * </p>
     */
    private final ConcurrentMap<String, Set<EventSinkMetadata>> m_eventSinkMetadata =
            new ConcurrentHashMap<String, Set<EventSinkMetadata>>();

    /**
     * <p>
     * Construct a {@link DefaultConfiguration} instance.
     * </p>
     */
    public DefaultConfiguration()
    {
        super();
    }

    /**
     * <p>
     * Construct a {@link DefaultConfiguration} instance.
     * </p>
     * 
     * @param initialContents the initial contents of the event sink metadata map.
     */
    public DefaultConfiguration(final Map<String, Set<EventSinkMetadata>> initialContents)
    {
        super();
        Guard.notNull("initialContents", initialContents);

        m_eventSinkMetadata.putAll(initialContents);
    }

    /* inherit javadoc */
    public Set<EventSinkMetadata> getEventSinkMetadata(final Object listener)
    {
        Guard.notNull("listener", listener);

        return getEventSinkMetadataForClass(listener.getClass());
    }

    /**
     * <p>
     * Add an event sink metadata object.
     * </p>
     * 
     * @param eventSinkMetadata the event sink metadata.
     */
    public void addEventSinkMetadata(final EventSinkMetadata eventSinkMetadata)
    {
        // Get the listener class and method signature.
        final String listenerClassName = eventSinkMetadata.getListenerClassName();
        final String methodSignature = eventSinkMetadata.getMethodSignature();
        if (LOG.isDebugEnabled())
        {
            LOG.debug(String.format("Adding event sink metadata [%s] for method %s.%s.", eventSinkMetadata,
                    listenerClassName, methodSignature));
        }

        // May have to retry this if there is contention on this listener class.
        while (true)
        {
            if (!m_eventSinkMetadata.containsKey(listenerClassName))
            {
                // No existing set for this listener class; try to add a new set.
                final Set<EventSinkMetadata> newMetadata = Collections.singleton(eventSinkMetadata);
                if (null == m_eventSinkMetadata.putIfAbsent(listenerClassName, newMetadata))
                {
                    // Successfully added our set.
                    break;
                }
            }
            else
            {
                // Existing set for this listener class; try to add and replace it.
                final Set<EventSinkMetadata> existingMetadata = m_eventSinkMetadata.get(listenerClassName);
                final Set<EventSinkMetadata> newMetadata = new HashSet<EventSinkMetadata>(existingMetadata.size() + 1);
                newMetadata.addAll(existingMetadata);
                newMetadata.add(eventSinkMetadata);
                if (m_eventSinkMetadata.replace(listenerClassName, existingMetadata, newMetadata))
                {
                    // Successfully added our set.
                    break;
                }
            }
        }
    }

    /**
     * <p>
     * Get a set containing zero or more {@link EventSinkMetadata} instances describing the event sinks that are
     * configured for a given application listener class.
     * </p>
     *
     * @param clazz the listener class.
     * @return {@link Set} of {@link EventSinkMetadata} instances.
     */
    protected Set<EventSinkMetadata> getEventSinkMetadataForClass(final Class<?> clazz)
    {
        // TODO: optimize/cache this.
        final Set<EventSinkMetadata> result = new HashSet<EventSinkMetadata>();
        for (final String nextClassName : ReflectionUtils.getAllSuperclassNames(clazz.getName()))
        {
            if (m_eventSinkMetadata.containsKey(nextClassName))
            {
                final Set<EventSinkMetadata> nextClassMetadata = m_eventSinkMetadata.get(nextClassName);
                result.addAll(nextClassMetadata);
            }
        }
        return result;
    }
}
